﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
namespace xunsearch.lib
{
    /// <summary>
    /// XS 索引管理
    /// 添加/删除/修改索引数据
    /// </summary>
    public class XSIndex:XSServer
    {
        private string _buf = "";
        private int _bufSize = 0;
        private bool _rebuild = false;
        private static List<XSServer> _adds = new List<XSServer>();

        public XSIndex() 
        {
        }
        public XSIndex(string conn, XS xs) : base(conn, xs)
        {

        }
        /// <summary>
        /// 增加一个同步索引服务器
        /// </summary>
        /// <param name="conn">索引服务端连接参数</param>
        /// <returns></returns>
        public XSServer addServer(string conn) 
        {
            XSServer srv = new XSServer(conn, xs);
            _adds.Add(srv);
            return srv;
        }
        /// <summary>
        /// 执行服务端指令并获取返回值
        /// 重写此方法是为了同步到额外增加的多个索引服务端
        /// </summary>
        /// <param name="cmd"></param>
        /// <param name="res_arg"></param>
        /// <param name="res_cmd"></param>
        /// <returns></returns>
        public XSCommand execCommand(int cmd, int res_arg = xs_cmd.CMD_NONE, int res_cmd = xs_cmd.CMD_OK) 
        {
            XSCommand res = base.execCommand(cmd, res_arg, res_cmd);
            foreach (XSServer srv in _adds) 
            {
                srv.execCommand(cmd, res_arg, res_cmd);
            }
            return res;
        }
        /// <summary>
        /// 完全清空索引数据
        /// 如果当前数据库处于重建过程中将禁止清空
        /// </summary>
        /// <returns>XSIndex 返回自身对象以支持串接操作</returns>
        public XSIndex clean() 
        {
            execCommand(xs_cmd.CMD_INDEX_CLEAN_DB, xs_cmd.CMD_OK, xs_cmd.CMD_OK_DB_CLEAN);
            return this;
        }
        /// <summary>
        /// 添加文档到索引中
        /// 特别要注意的是: 系统不会自动检测主键是否冲突, 即便已存在相同主键也会添加进去
        /// </summary>
        /// <param name="doc"></param>
        /// <returns>XSIndex 返回自身对象以支持串接操作</returns>
        public XSIndex add(XSDocument doc) 
        {
            return update(doc, true);
        }
        /// <summary>
        /// 更新索引文档
        /// 该方法相当于先根据主键删除已存在的旧文档, 然后添加该文档
        /// 如果你能明确认定是新文档, 则建议使用 {@link add}
        /// </summary>
        /// <param name="doc"></param>
        /// <param name="add">是否为新增文档, 已有数据中不存在同一主键的其它数据</param>
        /// <returns></returns>
        public XSIndex update(XSDocument doc, bool add = false) 
        {
            HttpContext.Current.Response.Write("<hr>XSindex.update() begin<br>");
            if (doc.beforeSubmit(this) == false) 
            {
                return this;
            }
            XSFieldMeta fid = xs.getFieldId();
            string key = doc.f(fid.__toString());
            if (key == null || key == "")
            {
                throw new Exception("Missing value of primary key (FIELD:" + fid + ")");
            }

            XSCommand cmd = new XSCommand(xs_cmd.CMD_INDEX_REQUEST, xs_cmd.CMD_INDEX_REQUEST_ADD);
            if (add == false) 
            {
                cmd.arg1 = xs_cmd.CMD_INDEX_REQUEST_UPDATE;
                cmd.arg2 = fid.vno;
                cmd.buf = key;
            }

            List<XSCommand> cmds = new List<XSCommand>();
            cmds.Add(cmd);

            //document cmds
            HttpContext.Current.Response.Write("document cmds<br>");
            foreach (var de in xs.getAllFields())
            {
                XSFieldMeta field = de.Value;
                string value = doc.f(field.__toString());
                if (value != "") 
                {                    
                    int varg = field.isNumeric() ? xs_cmd.CMD_VALUE_FLAG_NUMERIC : 0;
                    value = field.val(value);
                    if (!field.hasCustomTokenizer())
                    {
                        int wdf = field.weight | (field.withPos() ? xs_cmd.CMD_INDEX_FLAG_WITHPOS : 0);
                        if (field.hasIndexMixed())
                        {
                            cmds.Add(new XSCommand(xs_cmd.CMD_DOC_INDEX, wdf, XSFieldScheme.MIXED_VNO, value));
                        }
                        if (field.hasIndexSelf())
                        {
                            wdf |= field.isNumeric() ? 0 : xs_cmd.CMD_INDEX_FLAG_SAVEVALUE;
                            cmds.Add(new XSCommand(xs_cmd.CMD_DOC_INDEX, wdf, field.vno, value));
                        }

                        // add value
                        if (!field.hasIndexSelf() || field.isNumeric())
                        {
                            cmds.Add(new XSCommand(xs_cmd.CMD_DOC_VALUE, varg, field.vno, value));
                        }
                    }
                    else
                    {
                        // add index
                        if (field.hasIndex()) 
                        {
                            XSTokenizer to = (XSTokenizer)field.getCustomTokenizer();
                            List<string> terms = to.getTokens(value, doc);
                            //self: [bool term, NOT weight, NOT stem, NOT pos]
                            if (field.hasIndexSelf()) 
                            {
                                int wdf = field.isBoolIndex() ? 1 : (field.weight | xs_cmd.CMD_INDEX_FLAG_CHECKSTEM);
                                foreach (string term in terms) 
                                {
                                    
                                    if (term.Length > 200) 
                                    {
                                        continue;
                                    }
                                    string t = term.ToLower();
                                    cmds.Add(new XSCommand(xs_cmd.CMD_DOC_TERM, wdf, field.vno, t));
                                }
                            }
                            //mixed: [use default tokenizer]
                            if(field.hasIndexMixed())
                            {
                                string mtext = string.Join(" ", terms.ToArray());
                                cmds.Add(new XSCommand(xs_cmd.CMD_DOC_INDEX, field.weight, XSFieldScheme.MIXED_VNO, mtext));
                            }
                            
                        }
                        // add value
                        cmds.Add(new XSCommand(xs_cmd.CMD_DOC_VALUE, varg, field.vno, value));
                    }

                }
                //process add text
                HttpContext.Current.Response.Write("process add text");
                string text = doc.getAddIndex(field.__toString());
                if (text != "") 
                {
                    if (!field.hasCustomTokenizer())
                    {
                        int wdf = field.weight | (field.withPos() ? xs_cmd.CMD_INDEX_FLAG_WITHPOS : 0);
                        cmds.Add(new XSCommand(xs_cmd.CMD_DOC_INDEX, wdf, field.vno, text));
                    }
                    else 
                    {
                        //NOT pos
                        int wdf = field.isBoolIndex() ? 1 : (field.weight | xs_cmd.CMD_INDEX_FLAG_CHECKSTEM);
                        XSTokenizer xt = (XSTokenizer)field.getCustomTokenizer();
                        List<string> terms = xt.getTokens(text, doc);
                        foreach (string term in terms) 
                        {
                            if (term.Length > 200) 
                            {
                                continue;
                            }
                            cmds.Add(new XSCommand(xs_cmd.CMD_DOC_TERM, wdf, field.vno, term.ToLower()));
                        }

                    }
                    
                }
            }
            // submit cmd
            cmds.Add(new XSCommand(xs_cmd.CMD_INDEX_SUBMIT));

            // execute cmd
            if (_bufSize > 0)
            {
                //appendBuffer
            }
            else 
            {
                for (int i = 0; i < cmds.Count - 1; i++) 
                {
                    execCommand(cmds[i]);
                }
                execCommand(cmds[cmds.Count-1], xs_cmd.CMD_OK_RQST_FINISHED);
            }

            // after submit
            //doc.afterSubmit(this);

            return this;
        }

        /// <summary>
        /// 删除索引中的数据
        /// </summary>
        /// <param name="term"></param>
        /// <param name="field"></param>
        /// <returns></returns>
        public XSIndex del(string[] term, string field = "") 
        {
            XSFieldMeta fields = (field == "") ? xs.getFieldId() : xs.getField(field);
            List<XSCommand> cmds = new List<XSCommand>();
            
            foreach (string t in term) 
            {
                cmds.Add(new XSCommand(xs_cmd.CMD_INDEX_REMOVE, 0, fields.vno, t.ToLower()));
            }

            //combine multi commands into exdata
            if (_bufSize > 0) 
            {
                //appendBuffer();
            }
            else if (cmds.Count == 1)
            {
                execCommand(cmds[0], xs_cmd.CMD_OK_RQST_FINISHED);
            }
            else 
            {
                string cmds_str="";
                foreach(var _cmd in cmds)
                {
                    cmds_str+=_cmd.toString();
                }
                execCommand(new XSCommand(xs_cmd.CMD_INDEX_EXDATA, 0, 0, cmds_str), xs_cmd.CMD_OK_RQST_FINISHED);
            }

            return this;
        }

        /// <summary>
        /// 批量提交索引命令封包数据
        /// 把多个命令封包内容连续保存为文件或变量, 然后一次性提交以减少网络开销提升性能
        /// </summary>
        /// <param name="data">要提交的命令封包数据, 或存储命令封包的文件路径, 编码必须已经是 UTF-8</param>
        /// <param name="check_file">是否检测参数为文件的情况</param>
        /// <returns>返回自身对象以支持串接操作</returns>
        public XSIndex addExdata(string data, bool check_file = true) 
        {
            if (data.Length < 255 && check_file) 
            {
            }
            char[] a = data.Substring(0, 1).ToCharArray();
            int first = a[0];
            if (first != xs_cmd.CMD_IMPORT_HEADER
                && first != xs_cmd.CMD_INDEX_REQUEST
                && first != xs_cmd.CMD_INDEX_SYNONYMS
                && first != xs_cmd.CMD_INDEX_REMOVE
                && first != xs_cmd.CMD_INDEX_EXDATA) 
            {
                throw new Exception("Invalid start command of exdata (CMD:' " + first.ToString() + "')");
            }
            // create cmd & execute it
            execCommand(new XSCommand(xs_cmd.CMD_INDEX_EXDATA, 0, 0, data), xs_cmd.CMD_OK_RQST_FINISHED);
            return this;
        }

        /// <summary>
        /// 添加同义词
        /// </summary>
        /// <param name="raw">需要同义的原词, 英文词汇支持用空格分开多个单词并强制被转换为小写</param>
        /// <param name="synonym">同义词条, 最小语素, 勿带空格等分隔符</param>
        /// <returns>返回自身对象以支持串接操作</returns>
        public XSIndex addSynonym(string raw, string synonym) 
        {
            if (raw != "" && synonym != "") 
            {
                XSCommand cmd=new XSCommand(xs_cmd.CMD_INDEX_SYNONYMS, xs_cmd.CMD_INDEX_SYNONYMS_ADD, 0, raw, synonym);
                if (_bufSize > 0) 
                {
                    execCommand(cmd, xs_cmd.CMD_OK_RQST_FINISHED);
                }
            }
            return this;
        }

        /// <summary>
        /// 删除某个同义词
        /// </summary>
        /// <param name="raw">需要同义的原词, 英文词汇支持用空格分开多个单词并强制被转换为小写</param>
        /// <param name="synonym">要删除的同义词条, 默认 null 表示删除原词下的所有同义词</param>
        /// <returns></returns>
        public XSIndex delSynonym(string raw, string synonym="") 
        {
            if (raw != "") 
            {
                XSCommand cmd = new XSCommand(xs_cmd.CMD_INDEX_SYNONYMS, xs_cmd.CMD_INDEX_SYNONYMS_DEL, 0, raw, synonym);
                if (_bufSize > 0)
                {
                    //appendBuffer(cmd.toString());
                }
                else 
                {
                    execCommand(cmd, xs_cmd.CMD_OK_RQST_FINISHED);
                }
            }

            return this;
        }

        /// <summary>
        /// 设置当前索引库的分词复合等级
        /// 复合等级是 scws 分词粒度控制的一个重要参数, 是长词细分处理依据, 默认为 3, 值范围 0~15
        /// 注意: 这个设置仅直对当前索引库有效, 多次调用设置值被覆盖仅最后那次设置有效,
        /// 而且仅对设置之后提交的索引数据起作用, 如需对以前的索引数据生效请重建索引.
        /// </summary>
        /// <param name="level">要设置的分词复合等级</param>
        /// <returns></returns>
        public XSIndex setScwsMulti(int level) 
        {
            if (level >= 0 && level < 16) 
            {
                execCommand(new XSCommand(xs_cmd.CMD_SEARCH_SCWS_SET, xs_cmd.CMD_SCWS_SET_MULTI, level));
            }
            return this;
        }

        /// <summary>
        /// 获取当前索引库的分词复合等级
        /// </summary>
        /// <returns>返回当前库的分词复合等级</returns>
        public int getScwsMulti() 
        {
            XSCommand  res=execCommand(new XSCommand(xs_cmd.CMD_SEARCH_SCWS_GET, xs_cmd.CMD_SCWS_GET_MULTI), xs_cmd.CMD_OK_INFO);
            return 0;// todo
        }

        /// <summary>
        /// 开启索引命令提交缓冲区
        /// 为优化网络性能, 有必要先将本地提交的 add/update/del 等索引变动指令缓存下来, 
        /// 当总大小达到参数指定的 size 时或调用 {@link closeBuffer} 时再真正提交到服务器
        /// 注意: 此举常用于需要大批量更新索引时, 此外重复调用本函数是无必要的
        /// </summary>
        /// <param name="size">缓冲区大小, 单位: MB 默认为 4MB</param>
        /// <returns>返回自身对象以支持串接操作</returns>
        public XSIndex openBuffer(int size = 4) 
        {
            if (_buf != "") 
            {
                addExdata(_buf, false);
            }
            _bufSize = size << 20;
            _buf = "";
            return this;
        }

        /// <summary>
        /// 提交所有指令并关闭缓冲区
        /// 若未曾打开缓冲区, 调用本方法是无意义的
        /// </summary>
        /// <returns></returns>
        public XSIndex closeBuffer() 
        {
            return openBuffer(0);
        }

        /// <summary>
        /// 开始重建索引
        /// 此后所有的索引更新指令将写到临时库, 而不是当前搜索库, 重建完成后调用
        /// {@link endRebuild} 实现平滑重建索引, 重建过程仍可搜索旧的索引库,
        /// 如直接用 {@link clean} 清空数据, 则会导致重建过程搜索到不全的数据
        /// </summary>
        /// <returns></returns>
        public XSIndex beginRebuild() 
        {
            execCommand(new XSCommand(xs_cmd.CMD_INDEX_REBUILD, 0), xs_cmd.CMD_OK_DB_REBUILD);
            _rebuild = true;
            return this;
        }
        /// <summary>
        /// 完成并关闭重建索引
        /// 重建完成后调用, 用重建好的索引数据代替旧的索引数据
        /// </summary>
        /// <returns></returns>
        public XSIndex endRebuild() 
        {
            if (_rebuild) 
            {
                _rebuild = false;
                execCommand(new XSCommand(xs_cmd.CMD_INDEX_REBUILD, 1), xs_cmd.CMD_OK_DB_REBUILD);
            }
            return this;
        }

        /// <summary>
        /// 中止索引重建
        /// 丢弃重建临时库的所有数据, 恢复成当前搜索库, 主要用于偶尔重建意外中止的情况
        /// </summary>
        /// <returns></returns>
        public XSIndex stopRebuild() 
        {
            try
            {
                execCommand(new XSCommand(xs_cmd.CMD_INDEX_REBUILD, 2), xs_cmd.CMD_OK_DB_REBUILD);
                _rebuild = false;
            }
            catch (Exception ex) 
            {
                
            }

            return this;
        }

        /// <summary>
        /// 更改存放索引数据的目录
        /// 默认索引数据保存到服务器上的 db 目录, 通过此方法修改数据目录名
        /// </summary>
        /// <param name="name">数据库名称</param>
        /// <returns></returns>
        public XSIndex setDb(string name) 
        {
            execCommand(new XSCommand(xs_cmd.CMD_INDEX_SET_DB, 0, 0, name), xs_cmd.CMD_OK_DB_CHANGED);
            return this;
        }
        /// <summary>
        /// 强制刷新服务端当前项目的搜索日志
        /// </summary>
        /// <returns></returns>
        public bool flushLoggig() 
        {
            try
            {
                execCommand(xs_cmd.CMD_FLUSH_LOGGING, xs_cmd.CMD_OK_LOG_FLUSHED);

            }
            catch (Exception ex) 
            {
                return false;
            }
            return true;
        }

        /// <summary>
        /// 强制刷新服务端的当前库的索引缓存
        /// </summary>
        /// <returns>刷新成功返回 true, 失败则返回 false</returns>
        public bool flushIndex() 
        {
            try
            {
                execCommand(xs_cmd.CMD_INDEX_COMMIT, xs_cmd.CMD_OK_DB_COMMITED);
            }
            catch (Exception ex) 
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// 获取自定义词典内容
        /// </summary>
        /// <returns>自定义词库内容</returns>
        public string getCustomDict() 
        {
            XSCommand res = execCommand(xs_cmd.CMD_INDEX_USER_DICT, xs_cmd.CMD_OK_INFO);
            return "";
        }
        /// <summary>
        /// 设置自定义词典内容
        /// </summary>
        /// <param name="content"></param>
        public void setCustomDict(string content) 
        {
            execCommand(new XSCommand(xs_cmd.CMD_INDEX_USER_DICT, 1, 0, content), xs_cmd.CMD_OK_DICT_SAVED);
        }

        /// <summary>
        /// 关闭索引服务端连接
        /// </summary>
        /// <param name="ioerr"></param>
        public void close(bool ioerr = false) 
        {
            closeBuffer();
            base.close(ioerr);
        }

        /// <summary>
        /// 追加缓冲区命令数据
        /// 若增加后的数据长度达到缓冲区最大值则触发一次服务器提交.
        /// </summary>
        /// <param name="buf"></param>
        public void appendBuffer(string buf) 
        {
            _buf += buf;
            if (_buf.Length >= _bufSize) 
            {
                addExdata(_buf, false);
                _buf = "";
            }
        }

        public void __destruct() 
        {
            if (_rebuild) 
            {
                try
                {
                    endRebuild();
                }
                catch (Exception ex) 
                {
                }
            }
            foreach (XSServer srv in _adds) 
            {
                srv.close();
            }
            _adds = null;
            base.__destruct();
        }
    }
}
